﻿using System;
using System.Security.Cryptography;
using System.Text;
using System.Collections.Generic;
using System.Runtime.Serialization;
using System.Linq;
using System.Web;
using System.Web.Security;

namespace Gollie
{
	public class Ticket
	{
		private const int InitialCapacity = 32;
		private const int CapacityIncrement = 16;

		private byte[] _payLoad;
		private int _length;

		private static HashAlgorithm hashAlgorithm = HashAlgorithm.Create("SHA256");

        /// <summary>
        /// Initializes a new, empty, instance of the <see cref="Ticket"/> class.
        /// </summary>
        public Ticket()
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="Ticket"/> class.
        /// </summary>
        /// <param name="ticket">String representation of a Ticket.</param>
        public Ticket(string ticket)
        {
            _payLoad = Convert.FromBase64String(ticket);
            _length = _payLoad.Length;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="Ticket"/> class.
        /// </summary>
        /// <param name="ticketData">A byte array containing the Ticket data.</param>
        public Ticket(byte[] ticketData)
        {
            _payLoad = new byte[ticketData.Length];
            Buffer.BlockCopy(ticketData, 0, _payLoad, 0, ticketData.Length);
            _length = _payLoad.Length;
        }

        /// <summary>
        /// Returns the string representation of the Ticket.
        /// </summary>
        /// <returns></returns>
        public override string ToString()
        {
            return _length > 0 ? Convert.ToBase64String(_payLoad, 0, _length) : String.Empty;
        }

        /// <summary>
        /// Convert the given ticketString to a Ticket.
        /// </summary>
        /// <param name="ticketString">String containg the string representation of a Ticket.</param>
        /// <returns>A Ticket.</returns>
        public static Ticket Parse(string ticketString)
        {
            return new Ticket(ticketString);
        }

        public static bool TryParse(string ticketString, out Ticket ticket)
        {
            ticket = null;
            try
            {
                ticket = Parse(ticketString);
            }
            catch
            {
                return false;
            }

            return true;
        }

        /// <summary>
        /// Add the given value to the ticket.
        /// </summary>
        /// <param name="value">Value to be added to the ticket.</param>
        public void Push(bool value)
        {
            Push(BitConverter.GetBytes(value));
        }

        /// <summary>
        /// Add the given value to the ticket.
        /// </summary>
        /// <param name="value">Value to be added to the ticket.</param>
        public void Push(short value)
        {
            Push(BitConverter.GetBytes(value));
        }

        /// <summary>
        /// Add the given value to the ticket.
        /// </summary>
        /// <param name="value">Value to be added to the ticket.</param>
        public void Push(int value)
        {
            Push(BitConverter.GetBytes(value));
        }

        /// <summary>
        /// Add the given value to the ticket.
        /// </summary>
        /// <param name="value">Value to be added to the ticket.</param>
        public void Push(uint value)
        {
            Push(BitConverter.GetBytes(value));
        }

        /// <summary>
        /// Add the given value to the ticket.
        /// </summary>
        /// <param name="value">Value to be added to the ticket.</param>
        public void Push(long value)
        {
            Push(BitConverter.GetBytes(value));
        }

        /// <summary>
        /// Add the given value to the ticket.
        /// </summary>
        /// <param name="value">Value to be added to the ticket.</param>
        public void Push(string value)
        {
            if (value == null) throw new ArgumentNullException();
            byte[] bytes = Encoding.UTF8.GetBytes(value);
			if (bytes.Length > short.MaxValue) throw new TicketException("Ticket: string too long");
            Push(bytes);
            Push((short)bytes.Length);
        }

        /// <summary>
        /// Add the given value to the ticket.
        /// </summary>
        /// <param name="value">Value to be added to the ticket.</param>
        public void Push(decimal value)
        {
            var bits = Decimal.GetBits(value);
            Push(bits[0]);
            Push(bits[1]);
            Push(bits[2]);
            Push(bits[3]);
        }

        /// <summary>
        /// Add the given value to the ticket.
        /// </summary>
        /// <param name="value">Value to be added to the ticket.</param>
        public void Push(DateTime value)
        {
            Push(value.Ticks);
        }

        /// <summary>
        /// Add the given value to the ticket.
        /// </summary>
        /// <param name="value">Value to be added to the ticket.</param>
        public void Push(Guid value)
        {
            Push(value.ToByteArray());
        }

        /// <summary>
        /// Add the hash value of the ticket to the ticket.
        /// </summary>
        /// <param name="hashAlgorithm">The <see cref="HashAlgorithm"/> to use.</param>
        public void PushHash()
        {
            Push(hashAlgorithm.ComputeHash(_payLoad, 0, _length));
        }

        /// <summary>
        /// Add the given value to the ticket.
        /// </summary>
        /// <param name="value">Value to be added to the ticket.</param>
        public void Push(byte[] value)
        {
            Push(value, 0, value.Length);
        }

        /// <summary>
        /// Add the given buffer to the Ticket.
        /// </summary>
        /// <param name="buffer">Bytes to be added to the ticket.</param>
        /// <param name="offset">Offset in the given buffer of the first byte to push.</param>
        /// <param name="count">Number of bytes to push.</param>
        public void Push(byte[] buffer, int offset, int count)
        {
            if (_payLoad == null)
                _payLoad = new byte[InitialCapacity];
            if (_length + count > _payLoad.Length)
            {
                byte[] oldPayLoad = _payLoad;
                _payLoad = new byte[_payLoad.Length + (count > CapacityIncrement ? count : CapacityIncrement)];
                oldPayLoad.CopyTo(_payLoad, 0);
            }
            Buffer.BlockCopy(buffer, offset, _payLoad, _length, count);
            _length += count;
        }

        /// <summary>
        /// Remove the hash value at the end of the ticket and compare it with the actual hash.
        /// </summary>
        /// <param name="hashAlgorithm">The <see cref="HashAlgorithm"/> that was used.</param>
        /// <exception cref="WebException">If the hash value was invalid.</exception>
        public void PopHash()
        {
            int size = hashAlgorithm.HashSize / 8;
            if (_length < size) throw new TicketException("Transactie is niet valide");
            _length -= size;
            byte[] hash = hashAlgorithm.ComputeHash(_payLoad, 0, _length);
            for (int i = 0; i < size; i++)
				if (hash[i] != _payLoad[_length + i]) throw new TicketException("Transactie is niet valide");
        }

        /// <summary>
        /// Removes a <see cref="Guid"/> at the end of the <see cref="Ticket"/> and returns it.
        /// </summary>
        /// <returns>A <see cref="Guid"/> value.</returns>
        public Guid PopGuid()
        {
            return new Guid(PopBytes(16));
        }

        /// <summary>
        /// Removes a <see cref="String"/> at the end of the <see cref="Ticket"/> and returns it.
        /// </summary>
        /// <returns>A <see cref="String"/> value.</returns>
        public string PopString()
        {
            int count = PopInt16();
			if (_length < count || count < 0) throw new TicketException("Transactie is niet valide");
            _length -= count;
            return Encoding.UTF8.GetString(_payLoad, _length, count);
        }

        /// <summary>
        /// Removes a <see cref="Int16"/> at the end of the <see cref="Ticket"/> and returns it.
        /// </summary>
        /// <returns>A <see cref="Int16"/> value.</returns>
        public short PopInt16()
        {
			if (_length < 2) throw new TicketException("Transactie is niet valide");
            _length -= 2;
            return BitConverter.ToInt16(_payLoad, _length);
        }

        /// <summary>
        /// Removes a <see cref="Int32"/> at the end of the <see cref="Ticket"/> and returns it.
        /// </summary>
        /// <returns>A <see cref="Int32"/> value.</returns>
        public int PopInt32()
        {
			if (_length < 4) throw new TicketException("Transactie is niet valide");
            _length -= 4;
            return BitConverter.ToInt32(_payLoad, _length);
        }

        /// <summary>
        /// Removes a <see cref="UInt32"/> at the end of the <see cref="Ticket"/> and returns it.
        /// </summary>
        /// <returns>A <see cref="UInt32"/> value.</returns>
        public uint PopUInt32()
        {
			if (_length < 4) throw new TicketException("Transactie is niet valide");
            _length -= 4;
            return BitConverter.ToUInt32(_payLoad, _length);
        }

        /// <summary>
        /// Removes a <see cref="Int64"/> at the end of the <see cref="Ticket"/> and returns it.
        /// </summary>
        /// <returns>A <see cref="Int64"/> value.</returns>
        public long PopInt64()
        {
			if (_length < 8) throw new TicketException("Transactie is niet valide");
            _length -= 8;
            return BitConverter.ToInt64(_payLoad, _length);
        }

        /// <summary>
        /// Removes a <see cref="bool"/> at the end of the <see cref="Ticket"/> and returns it.
        /// </summary>
        /// <returns>A <see cref="bool"/> value.</returns>
        public bool PopBoolean()
        {
			if (_length < 1) throw new TicketException("Transactie is niet valide");
            _length -= 1;
            return BitConverter.ToBoolean(_payLoad, _length);
        }

        /// <summary>
        /// Removes a <see cref="decimal"/> at the end of the <see cref="Ticket"/> and returns it.
        /// </summary>
        /// <returns>A <see cref="decimal"/> value.</returns>
        public decimal PopDecimal()
        {
            var bits = new int[4];
            bits[3] = PopInt32();
            bits[2] = PopInt32();
            bits[1] = PopInt32();
            bits[0] = PopInt32();
            return new decimal(bits);
        }

        /// <summary>
        /// Removes a <see cref="DateTime"/> at the end of the <see cref="Ticket"/> and returns it.
        /// </summary>
        /// <returns>A <see cref="DateTime"/> value.</returns>
        public DateTime PopDateTime()
        {
            var ticks = PopInt64();
            return new DateTime(ticks);
        }

        /// <summary>
        /// Removes a given number of bytes at the end of the <see cref="Ticket"/> and returns it as a byte array.
        /// </summary>
        /// <param name="count">Number of bytes to pop.</param>
        /// <returns>A byte array value.</returns>
        public byte[] PopBytes(int count)
        {
            var result = new byte[count];
            PopBytes(result, 0, count);
            return result;
        }

        /// <summary>
        /// Removes a given number of bytes at the end of the <see cref="Ticket"/> and copies them to the given buffer.
        /// </summary>
        /// <param name="buffer">Buffer to write the popped bytes to.</param>
        /// <param name="offset">Offset in the given buffer to start copying.</param>
        /// <param name="count">Number of bytes to pop.</param>
        public void PopBytes(byte[] buffer, int offset, int count)
        {
			if (_length < count) throw new TicketException("Transactie is niet valide");
            _length -= count;
            Buffer.BlockCopy(_payLoad, _length, buffer, offset, count);
        }

        /// <summary>
        /// Returns the complete payload of the Ticket as a byte array.
        /// Unlike the pop methods, this does not affect the contents of the Ticket.
        /// </summary>
        /// <returns>A byte array containing the payload of the Ticket.</returns>
        public byte[] GetBytes()
        {
            var result = new byte[_length];
            Buffer.BlockCopy(_payLoad, 0, result, 0, _length);
            return result;
        }

        /// <summary>
        /// Returns a copy of the Ticket.
        /// </summary>
        /// <returns>The copy of the Ticket.</returns>
        public Ticket Clone()
        {
            var clone = new Ticket();
            clone.Push(_payLoad, 0, _length);
            return clone;
        }

		//internal void Store()
		//{
		//	Database.Queries.InsertTicket(this.GetBytes());
		//}

        /// <summary>
        /// Performs a cryptographic transformation on the ticket payload.
        /// </summary>
        /// <param name="transform">The <see cref="ICryptoTransform"/> object to be used for the transformation.</param>
        public void Transform(ICryptoTransform transform)
        {
            if (transform == null) throw new ArgumentNullException();

            if (transform.CanTransformMultipleBlocks)
            {
                _payLoad = transform.TransformFinalBlock(_payLoad, 0, _length);
                _length = _payLoad.Length;
            }
            else
            {
                var outputBuffer = new byte[(_length / transform.InputBlockSize + 1) * transform.OutputBlockSize];

                var inputOffset = 0;
                var inputLength = _length;
                var outputLength = 0;
                while (inputLength > transform.InputBlockSize)
                {
                    outputLength += transform.TransformBlock(_payLoad, inputOffset, transform.InputBlockSize, outputBuffer, outputLength);
                    inputOffset += transform.InputBlockSize;
                    inputLength -= transform.InputBlockSize;
                }
                byte[] finalBlock = transform.TransformFinalBlock(_payLoad, inputOffset, inputLength);
                finalBlock.CopyTo(outputBuffer, outputLength);
                outputLength += finalBlock.Length;
                _payLoad = outputBuffer;
                _length = outputLength;
            }
        }

	}

	public class TicketException : Exception
	{
		public TicketException(string msg)
			: base(msg)
		{
		}
	}
}